import { ElementRef, Injectable } from '@angular/core';
// import { NgForm, NgModel } from '@angular/forms';
import { DatePipe } from '@angular/common';

import { DateFormatConfig } from '../config/date-format.config';
import { FormGroup } from '@angular/forms';

@Injectable()
export class CommonService {
    constructor() { }

    //#region sessionStorage
    /**
     * Saves the data on the local client side sessionStorage.
     * @param key The key string is the identifier of the data.
     * @param data The data which will be saved.
     */
    saveSessionData(key: string, data: any): void {
        sessionStorage.setItem(key, JSON.stringify(data));
    }

    /**
     * Loads the specific data from the local client side sessionStorage.
     * @param key The key string is the identifier of the data.
     * @param defaultValue Optional, will returned if there is no value for provided key found.
     * @return The data object.
     */
    loadSessionData(key: string, defaultValue?: string): any {
        return JSON.parse(sessionStorage.getItem(key)) || defaultValue;
    }

    /**
     * Removes the specific data from the local client side sessionStorage.
     * @param key The key string is the identifier of the data.
     * @return The data object.
     */
    removeSessionData(key: string): void {
        sessionStorage.removeItem(key);
    }
    //#endregion

    //#region localStorage
    /**
     * Saves the data on the local client side localStorage.
     * @param key The key string is the identifier of the data.
     * @param data The data which will be saved.
     */
    saveLocalData(key: string, data: any): void {
        localStorage.setItem(key, JSON.stringify(data));
    }

    /**
     * Loads the specific data from the local client side localStorage.
     * @param key The key string is the identifier of the data.
     * @param defaultValue Optional, will returned if there is no value for provided key found.
     * @return The data object.
     */
    loadLocalData(key: string, defaultValue?: string): any {
        return JSON.parse(localStorage.getItem(key)) || defaultValue;
    }

    /**
     * Removes the specific data from the local client side localStorage.
     * @param key The key string is the identifier of the data.
     * @return The data object.
     */
    removeLocalData(key: string): void {
        localStorage.removeItem(key);
    }
    //#endregion

    /**
     * Appends the script tag as child to the provided element.
     * @param elementRef The ElementRef of a DOM.
     * @param urls Script files' urls. The script files must be put under the assets folder and without leading slash. Example: 'js/sidebarmenu.js'
     */
    appendScriptChildToElement(elementRef: ElementRef, urls: string[]): void {
        this.appendScriptChild(elementRef.nativeElement, urls);
    }

    /**
     * Appends the script tag as child to the html document body.
     * @param urls Script files' urls. The script files must be put under the assets folder and without leading slash. Example: 'js/sidebarmenu.js'
     */
    appendScriptChildToDocumentBody(urls: string[]): void {
        this.appendScriptChild(document.body, urls);
    }

    /**
     * Appends the script tag as child to the provided container, element or html document body.
     * @param container The ElementRef of a DOM or the html document body.
     * @param urls Script files' urls. The script files must be put under the assets folder and without leading slash. Example: 'js/sidebarmenu.js'
     */
    appendScriptChild(container: any, urls: string[]): void {
        let script: HTMLScriptElement;

        // IMPORTANT!!
        // NEVER USE THE URL BEGINS WITH '/assets'!
        // The file url MUST begin with './assets' because the file relative path will change after build.
        // './assets' will point to the correct folder whether the app is deployed to the root of the nginx or the sub-directory after build.
        // Otherwise the file will NOT be found due to the incorrect path reference.
        // Example:
        // The root of the nginx: http://192.168.0.1:82
        // The sub-directory of the nginx: http://192.168.0.1:82/my-dream-app. When use '/assets' the file will be pointed to http://192.168.0.1:82/assets and cause 404 error.

        urls.forEach((url: string) => {
            script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = `./assets/${url}`;
            container.appendChild(script);
        });
    }

    /**
     * Checks if the input object is null or undefined.
     * @param input The input object.
     * @return The boolean result. True means the input is null or undefined. False means NOT.
     */
    isNullOrUndefined(input: any): boolean {
        return input === null || input === undefined;
    }

    /**
     * Checks if the input string is null, undefined or empty string.
     * @param input The input string.
     * @return The boolean result. True means the input is null, undefined or empty string. False means NOT.
     */
    isNullOrUndefinedOrEmptyString(input: any): boolean {
        return this.isNullOrUndefined(input) || input === '';
    }

    /**
     * Checks the input to see if its type is string and the value is not empty.
     * @param input The input instance will be checked.
     * @return True means the input is string type and it is not empty.
     */
    isNotEmptyString(input: any): boolean {
        return typeof (input) === 'string' && input !== '';
    }

    /**
     * Executes the function by using its name.
     * @param scope The function scope instance.
     * @param fnName The string of the function name.
     * @param param The parameter that the function might accept.
     * @returns Any data might be returned by this function.
     */
    executeFunctionByName(scope: any, fnName: string, param?: any): any {
        if (!this.isNullOrUndefined(scope) && !this.isNullOrUndefinedOrEmptyString(fnName)) {
            return scope[fnName](param);
        } else {
            return null;
        }
    }

    /**
     * Calculates the percentage based on the provided dividend and divisor.
     * @param dividend The number instance.
     * @param divisor The number instance.
     * @returns The rounded result.
     */
    calculatePercentage(dividend: number, divisor: number): number {
        return Math.round(100 * dividend / divisor);
    }

    /**
     * Gets current device system date.
     * @returns The date instance.
     */
    getCurrentDate(): Date {
        return new Date();
    }

    /**
     * Clones the input date and returns a new Date instance with the same value.
     * @param date The source Date instance.
     * @returns The new Date instance with the same value with input Date.
     */
    cloneDate(date: Date): Date {
        return new Date(date.getTime());
    }

    /**
     * Adds specific seconds to provided Date instance and returns as a new Date instance.
     * @param date The source Date instance.
     * @param seconds The number of seconds will be added to the provided Date intance.
     * @returns The new Date instance with added seconds to the provided Date.
     */
    addSeconds(date: Date, seconds: number): Date {
        // Clones the input Date instance.
        let newDate = this.cloneDate(date);

        // Adds the specific seconds to the input Date.
        newDate.setSeconds(newDate.getSeconds() + seconds);

        // Returns the new Date instance.
        return newDate;
    }

    /**
     * Formats the input date with provided format.
     * @returns The date instance.
     * @param format The string of the date format.
     * @returns The formatted date string.
     */
    formatDate(date: Date, format: string): string {
        return new DatePipe('en-US').transform(date, format);
    }

    /**
     * Gets the provided date timestamp string.
     * @returns The timestamp string.
     */
    getTimestamp(date: Date): string {
        return this.formatDate(date, DateFormatConfig.timestampFormat);
    }

    /**
     * Gets the current date timestamp string.
     * @returns The timestamp string.
     */
    getCurrentTimestamp(): string {
        return this.getTimestamp(this.getCurrentDate());
    }

    /**
     * Gets a random number within the provided range.
     * @param min The min number of the range beginning.
     * @param max The min number of the range ending.
     * @returns The generated random number within the provided range.
     */
    getRandomNumberWithinRange(min: number, max: number): number {
        return Math.floor(Math.random() * (max - min + 1) + min);
    }

    /**
     * Angular does not support iteration which goes through numbers just like for loop.
     * This method creates number array for ngFor to similate this kind of action.
     * Example with ngFor: *ngFor="let item of createRangeForNgFor(5); let currentElementIndex = index"
     * @param itemCount How many times this loop would goes through.
     * @returns The array contains numbers.
     */
    createRangeForNgFor(itemCount: number): number[] {
        let numbers: number[] = [];

        for (let i = 0; i < itemCount; i++) {
            numbers.push(i);
        }

        return numbers;
    }

    /**
     * Accepts an array which might have the duplicate items and returns an array with unique items along.
     * @param array The array which might have the duplicate items.
     * @returns An array with unique items along.
     */
    getUniqueArray(array: any[]): any[] {
        return array.filter((value: any, index: number, self: any[]) => self.indexOf(value) === index);
    }

    /**
     * Converts all the new line in the input string content to the provided replaced content.
     * @param content The input string content which contains new line to be replaced.
     * @param replacedContent The string content will replace the new line in the input string content.
     * @returns The string content with replaced content.
     */
    convertNewLine(content: string, replacedContent: string): string {
        if (content) {
            return content.replace(/(?:\r\n|\r|\n)/g, replacedContent);
        } else {
            return replacedContent;
        }
    }

    /**
     * Converts all the new line in the input string content to html <br> element.
     * @param content The input string content which contains new line to be replaced.
     * @returns The string content with replaced content.
     */
    convertNewLineToHtmlElement(content: string): string {
        return this.convertNewLine(content, '<br>');
    }

    /**
     * This will trim all string properties value in the input object.
     * This will NOT process nested properties.
     * @param obj The object string properties values will be trimmed.
     */
    trimObjectStringProperties(obj: Object): void {
        Object.keys(obj).forEach(p => {
            if (typeof obj[p] === 'string') {
                obj[p] = obj[p].trim();
            }
        });
    }

    /**
     * Vallidates the provided form field.
     * @param field The NgModel instance of a form field. HTML Example: #input="ngModel" which is a reference to an 
     * Angular NgModel directive with the ability to track the value and validity of every control in the form.
     * @return True means this form field is valid. False means invalid.
     */
    // validateFormField(form: NgForm, field: NgModel): boolean {
    //     return (form.submitted || field.dirty) && field.valid;
    // }

    /**
     * Starts to validate the input form group.
     * @param formGroup 
     */
    validateForm(formGroup: FormGroup): void {
        for (const i in formGroup.controls) {
            formGroup.controls[i].markAsDirty();
            formGroup.controls[i].updateValueAndValidity();
        }
    }
}